// =================================================================================================
// Copyright 2012 Twitter, Inc.
// -------------------------------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this work except in compliance with the License.
// You may obtain a copy of the License in the LICENSE file, or at:
//
//  http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =================================================================================================

package com.twitter.common.logging;

import javax.annotation.Nullable;

import com.google.common.base.Objects;
import com.google.common.base.Throwables;

import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

/**
 * A utility that can format log records to match the format generated by glog:
 * <pre>
 * I0218 17:36:47.461 (source) (message)
 * </pre>
 */
public final class Glog {

  /**
   * Classifies the importance of a log message.
   */
  public enum Level {

    /**
     * Indicates the message's classification is unknown.  This most likely indicates a
     * configuration or programming error that can be corrected by mapping the underlying log
     * system's level appropriately.
     */
    UNKNOWN('U'),

    /**
     * Indicates the message is for debugging purposes only.
     */
    DEBUG('D'),

    /**
     * Indicates a message of general interest.
     */
    INFO('I'),

    /**
     * Indicates a warning message likely worth of attention.
     */
    WARNING('W'),

    /**
     * Indicates an unexpected error.
     */
    ERROR('E'),

    /**
     * Indicates a fatal exception generally paired with actions to shut down the errored process.
     */
    FATAL('F');

    final char label;

    private Level(char label) {
      this.label = label;
    }
  }

  /**
   * An object that can provide details of a log record.
   *
   * @param <T> The type of log record the formatter handles.
   */
  public interface Formatter<T> {

    /**
     * Gets the message contained in the log record.
     *
     * @param record The record to extract a message from.
     * @return The formatted message.
     */
    String getMessage(T record);

    /**
     * Gets the class name of the class that sent the log record for logging.
     *
     * @param record The record to extract a producing class name from.
     * @return The producing class if known; otherwise {@code null}.
     */
    @Nullable
    String getClassName(T record);

    /**
     * Gets the name of the method of within the class that sent the log record for logging.
     *
     * @param record The record to extract a producing method name from.
     * @return The producing method name if known; otherwise {@code null}.
     */
    @Nullable
    String getMethodName(T record);

    /**
     * Gets the level of the log record.
     *
     * @param record The record to extract a log level from.
     * @return The record's log level. Can be {@code null} or {@link Level#UNKNOWN} if unknown.
     */
    @Nullable
    Level getLevel(T record);

    /**
     * Gets the timestamp in milliseconds since the epoch when the log record was generated.
     *
     * @param record The record to extract a time stamp from.
     * @return The log record's birth date.
     */
    long getTimeStamp(T record);

    /**
     * Gets the id of the thread that generated the log record.
     *
     * @param record The record to extract a thread id from.
     * @return The id of the thread that generated the log record.
     */
    long getThreadId(T record);

    /**
     * Gets the exception associated with the log record if any.
     *
     * @param record The record to extract an exception from.
     * @return The exception associated with the log record; may be {@code null}.
     */
    @Nullable
    Throwable getThrowable(T record);
  }

  private static final DateTimeFormatter DATE_TIME_FORMATTER =
      DateTimeFormat.forPattern("MMdd HH:mm:ss.SSS").withZone(DateTimeZone.UTC);

  private static final int BASE_MESSAGE_LENGTH =
      1     // Level char.
      + 4   // Month + day
      + 1   // space
      + 12  // Timestamp
      + 1   // space
      + 6   // THREAD
      + 4   // Room for thread ID.
      + 1;  // space

  /**
   * Converts the given log record into a glog format log line using the given formatter.
   *
   * @param formatter A formatter that understands how to unpack the given log record.
   * @param record A structure containing log data.
   * @param <T> The type of log record.
   * @return A glog formatted log line.
   */
  public static <T> String formatRecord(Formatter<T> formatter, T record) {
    String message = formatter.getMessage(record);
    int messageLength = BASE_MESSAGE_LENGTH
        + 2  // Colon and space
        + message.length();

    String className = formatter.getClassName(record);
    String methodName = null;
    if (className != null) {
      messageLength += className.length();
      methodName = formatter.getMethodName(record);
      if (methodName != null) {
        messageLength += 1;  // Period between class and method.
        messageLength += methodName.length();
      }
    }

    StringBuilder sb = new StringBuilder(messageLength)
        .append(Objects.firstNonNull(formatter.getLevel(record), Level.UNKNOWN).label)
        .append(DATE_TIME_FORMATTER.print(formatter.getTimeStamp(record)))
        .append(" THREAD")
        .append(formatter.getThreadId(record));

    if (className != null) {
      sb.append(' ').append(className);
      if (methodName != null) {
        sb.append('.').append(methodName);
      }
    }

    sb.append(": ").append(message);
    Throwable throwable = formatter.getThrowable(record);
    if (throwable != null) {
      sb.append('\n').append(Throwables.getStackTraceAsString(throwable));
    }

    return sb.append('\n').toString();
  }

  private Glog() {
    // utility
  }
}
